<?php
namespace Tlf\Tester;
/**
* See the Exceptions trait for exception-based assertions
* See the `Other` trait for invert(), disable(), and other methods that are important but are not assertions
*/
trait Assertions {
/**
* Call pre-defined global functions as assertions
*
* @example Use PHP's `in_array()` function as a test: `$this->in_array('value', $array_to_test);`
*/
public function __call($method, $args){
if (!function_exists($method)){
throw new \Exception("'$method' is not a valid assertion, but you can use any existing function (such as in_array()) as an assertion if it returns truthy/falsey values.");
}
$didPass = $method(...$args);
$this->handleDidPass($didPass);
$c = count($args);
echo "'$method'([$c arguments]) ";
}
public function isInstanceOf($object, $shouldBe){
$class = get_class($object);
if ($object instanceof $shouldBe){
$this->handleDidPass(true);
echo "'$class' is instanceof '$shouldBe'";
} else {
$this->handleDidPass(false);
echo "'$class' is NOT instanceof '$shouldBe'";
}
}
public function is_object($object){
if (is_object($object)){
$this->handleDidPass(true);
echo "Is an object";
} else {
$this->handleDidPass(false);
echo "Is not an object";
}
}
public function is_false($value){
$this->handleDidPass($value === false);
echo "is_false()";
}
public function is_true($value){
$this->handleDidPass($value === true);
echo "is_true()";
}
/**
* @param $str the string to check within
* @param $target a target string (or array of strings) to verify exist within $str
* @param $strings additional strings to check exist within $str
*/
public function str_contains($str, $target, ...$strings){
if (count($strings)>0)$target = [$target,...$strings];
if (is_array($target)){
$success = true;
foreach ($target as $strTarget){
$oneSuccess = $this->str_contains($str,$strTarget);
if (!$oneSuccess)$success = false;
}
return $success;
}
$pass = false;
if (strpos($str,$target)!==false)$pass=true;
$this->handleDidPass($pass);
if ($pass){
echo "String contains '$target'";
} else {
echo "String does not contain '$target'";
}
return $pass;
}
/**
* Make sure `$str` does not contain `$target`
*/
public function str_not_contains(string $str, $target, ...$strings){
if (count($strings)>0)$target = [$target,...$strings];
if (is_array($target)){
$success = true;
foreach ($target as $strTarget){
$oneSuccess = $this->str_not_contains($str,$strTarget);
if (!$oneSuccess)$success = false;
}
return $success;
} else $target = (string)$target;
$pass = true;
if (strpos($str,$target)!==false)$pass=false;
$this->handleDidPass($pass);
if ($pass){
echo "String does not contain '$target'";
} else {
echo "String contains '$target'";
}
return false;
}
/**
* Compares bools, strings, files (prefix path with `file://`), arrays, objects, and whatever else.
*
* @param $target the value you want
* @param $actual the value you have
* @param $strict true/false for strict (`===`) or non-strict (`==`) comparison.
*/
public function compare($target, $actual,$strict=false){
if (!$strict&&is_string($target)&&substr($target,0,7)=='file://'){
$file = substr($target,7);
if (!is_file($file)){
throw new \Exception("{$target} is not a file. ");
}
$ext = substr($file,-4);
if ($ext=='.php'){
ob_start();
require($file);
$target=ob_get_clean();
}
else $target=file_get_contents($file);
}
if (!$strict&&is_string($actual)&&substr($actual,0,7)=='file://'){
$file = substr($actual,7);
if (!is_file($file)){
throw new \Exception("{$actual} is not a file. ");
}
$ext = substr($file,-4);
if ($ext=='.php'){
ob_start();
require($file);
$actual=ob_get_clean();
}
else $actual=file_get_contents($file);
}
if (!$strict&&is_string($target)&&is_string($actual)){
$target = trim($target);
$actual = trim($actual);
}
$pass = false;
if ($strict&&($target===$actual))$pass = true;
else if (!$strict&&$target==$actual)$pass = true;
$target = $this->comparisonOutput($target);
$actual = $this->comparisonOutput($actual);
// echo "Strict: ".($strict ? 'true' : 'false');
$this->handleDidPass($pass);
if ($strict)echo " strict comparison";
$this->targetVsActualOutput($target, $actual);
return $pass;
}
/**
* Simply compare the values
* @param $target the value you want
* @param $actual the value you have
* @param $strict TRUE to use `===`. FALSE to use `==`
*/
public function compare_raw($target, $actual, $strict=false){
$pass = $strict ? $target === $actual
: $target == $actual;
ob_start();
var_dump($target);
$target_str = substr(ob_get_clean(), 0,500);
ob_start();
var_dump($actual);
$actual_str = substr(ob_get_clean(), 0,500);
$this->handleDidPass($pass);
$this->targetVsActualOutput(
$target_str,
$actual_str
);
return $pass;
}
/**
* Simply compare two arrays. Prints objects in the array as `ClassName#spl_object_id()`
*
* @param $target the array you want
* @param $actual the array you have
* @param $strict TRUE to use `===`. FALSE to use `==`
*/
public function compare_arrays($target, $actual, $strict=false){
$pass = $strict ? $target === $actual
: $target == $actual;
$target_str = print_r($this->printable_array($target), true);
$actual_str = print_r($this->printable_array($actual), true);
$this->handleDidPass($pass);
$this->targetVsActualOutput(
$target_str,
$actual_str
);
return $pass;
}
/**
* Simply compare two objects. Prints objects as `ClassName#spl_object_id()`
*
* @param $target the object you want
* @param $actual the object you have
* @param $strict TRUE to use `===`. FALSE to use `==`
*/
public function compare_objects($target, $actual, $strict=false){
$pass = $strict ? $target === $actual
: $target == $actual;
$target_str = !is_object($target) ? 'not-an-object' : get_class($target).'#'.spl_object_id($target);
$actual_str = !is_object($actual) ? 'not-an-object:'.gettype($actual) : get_class($actual).'#'.spl_object_id($actual);
$this->handleDidPass($pass);
$this->targetVsActualOutput(
$target_str,
$actual_str
);
return $pass;
}
/**
*
* @return true if tests passes, false otherwise
*/
public function compare_object_properties(object $object1, object $object2): bool {
$diff = [];
$same = [];
$obj1_id = get_class($object1).'#'.spl_object_id($object1);
$obj2_id = get_class($object2).'#'.spl_object_id($object2);
$reflection1 = new \ReflectionObject($object1);
$reflection2 = new \ReflectionObject($object2);
// Get all properties, including private and protected ones
$properties1 = $reflection1->getProperties();
$properties2 = $reflection2->getProperties();
foreach ($properties1 as $property) {
$propertyName = $property->getName();
// Make private and protected properties accessible
$property->setAccessible(true);
$v1 = $property->isInitialized($object1) ? $property->getValue($object1) : "UNDEFINED_OBJECT_PROPERTY";
$v2 = $property->isInitialized($object2) ? $property->getValue($object2) : "UNDEFINED_OBJECT_PROPERTY";
if ($v1!=$v2){
$diff[$propertyName] = [
'obj1' => $property->getValue($object1),
'obj2' => $property->getValue($object2),
];
} else {
$same[$propertyName] = 'VALUES MATCH';
}
}
foreach ($properties2 as $property) {
$propertyName = $property->getName();
if (isset($diff[$propertyName])
||isset($same[$propertyName])
)continue;
$v1 = $property->isInitialized($object1) ? $property->getValue($object1) : "UNDEFINED_OBJECT_PROPERTY";
$v2 = $property->isInitialized($object2) ? $property->getValue($object2) : "UNDEFINED_OBJECT_PROPERTY";
if ($v1!=$v2){
$diff[$propertyName] = [
'old' => $property->getValue($object1),
'new' => $property->getValue($object2),
];
} else {
$same[$propertyName] = 'VALUES MATCH';
}
}
if (count($diff) == 0){
echo "\nZero diffs between object properties for $obj1_id and $obj2_id";
$this->handleDidPass(true);
return true;
}
echo "\nDiffs exist between object properties on $obj1_id and $obj2_id:\n";
print_r($diff);
$this->handleDidPass(false);
return false;
}
public function compare_json($target, $actual, $strict=false){
if (is_string($target))$target = json_decode($target, true);
if (is_string($actual))$actual = json_decode($actual, true);
$this->compare($target, $actual, $strict);
}
/**
* Trim all lines in $str and $target and compare the resulting strings.
* @param $str The output you have
* @param $target The output you want
*/
public function str_contains_lines($str, $target){
$filter = function($v){return trim($v)!='';};
$target_lines = explode("\n", $target);
$target_lines = array_filter($target_lines, $filter);
$target_lines = array_map('trim', $target_lines);
$actual_lines = explode("\n", $str);
$actual_lines = array_filter($actual_lines, $filter);
$actual_lines = array_map('trim',$actual_lines);
$actual_lines = array_values($actual_lines);
$target_lines = array_values($target_lines);
$target_lines = implode("\n", $target_lines);
$actual_lines = implode("\n", $actual_lines);
// $this->targetVsActualOutput($target_lines, $actual_lines);
$this->str_contains($actual_lines, $target_lines);
}
/**
* Compare all non-empty lines after `trim()`ing each of them.
* @param $target the lines you want
* @param $actual the lines you have
*/
public function compare_lines($target, $actual){
$filter = function($v){return trim($v)!='';};
$target_lines = explode("\n", $target);
$target_lines = array_filter($target_lines, $filter);
$target_lines = array_map('trim', $target_lines);
$actual_lines = explode("\n", $actual);
$actual_lines = array_filter($actual_lines, $filter);
$actual_lines = array_map('trim',$actual_lines);
$actual_lines = array_values($actual_lines);
$target_lines = array_values($target_lines);
$passed = $target_lines == $actual_lines;
$this->handleDidPass($passed);
$target_lines = implode("\n", $target_lines);
$actual_lines = implode("\n", $actual_lines);
$this->targetVsActualOutput($target_lines, $actual_lines);
}
/**
* create a dump that converts objects to simple strings referencing their internal object id, so deep nesting is handled
*/
public function compare_dump($target, $actual){
$dumped_target = $this->dump_value($target);
$dumped_actual = $this->dump_value($actual);
$this->compare($dumped_target, $dumped_actual);
}
}